package utils;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import objects.AIPlayer;
import objects.Board;
import objects.BoardData;
import objects.Card;
import objects.CardType;
import objects.Character;
import objects.Envelope;
import objects.HumanPlayer;
import objects.Player;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class FileHandler {

	 /**
     * @throws Exception 
     */
    public static BoardData readXml(FileInputStream xmlFile) throws Exception
    {
    	List<Player> players = null;
    	Envelope env = null;
    	BoardData bdRetVal = null;
    	Document gameInfo = null;

    	try
    	{
    		gameInfo = parseXMLFromFile(xmlFile);
    	}
	    catch (SAXException ex) {
	    	throw new Exception("File is not a xml file");
	    }
		catch(Exception ex)
    	{
    		throw new Exception("Failed to parse XML. Make sure it is valid.");
    	}
		
    	if (gameInfo != null)
		{
			//load each player and player's cards.
			NodeList nodes = (NodeList) getNodesetFromDocument(gameInfo,"//players/player");
			if (nodes.getLength() == 0)
			{
					
				throw new Exception("XML File has no players");
			}
			else
			{
                // Iterate on all nodes and create players.
				players = new ArrayList<Player>();
				
				Board.getInstance().setNumberOfPlayersTotal(nodes.getLength());
				
				int computerPlayers = 0;
				
    			for (int i = 0; i < nodes.getLength(); i++) 
    			{
    				// Get a player's attributes
    				// NOT ALL WEAPON/CHAR/ROOM cards associated with players!
    				String character = nodes.item(i).getAttributes().getNamedItem("character").getNodeValue();
    				String name = nodes.item(i).getAttributes().getNamedItem("name").getNodeValue();
    				String type = nodes.item(i).getAttributes().getNamedItem("type").getNodeValue();
    				
    				Character playerCharacter = CaseMachine.getCharFromString(character);
    				
    				Player plPlayerToAdd = null;
    				
    				if (type.toUpperCase().equals("HUMAN"))
    				{
    					plPlayerToAdd = new HumanPlayer(i+1, name, playerCharacter);
    				}
    				else
    				{
    					plPlayerToAdd = new AIPlayer(i+1, name, playerCharacter);
    					computerPlayers++;
    				}
    				
    				plPlayerToAdd.setOrder(i+1);
    				plPlayerToAdd.setType(type.toUpperCase());
    				
    				// Iterate on the cards of the current player.
    				NodeList cardNodes = (NodeList) getNodesetFromDocument(nodes.item(i),"cards/card");
    				Integer t = cardNodes.getLength();
    				List<Card> lstCards = new ArrayList<Card>();
    				for (int j = 0; j < t; j++) 
	    			{
    					String cardType = cardNodes.item(j).getAttributes().getNamedItem("type").getNodeValue();
    					String cardValue = cardNodes.item(j).getAttributes().getNamedItem(cardType).getNodeValue();
    					// After getting a card data, we need to parse its text
    					// to a proper card.
    					Card cardToAdd = null;		
    					try
						{
	    					switch (cardType) 
    				        {
    				            case "weapon":
    				            	{
    				            		cardToAdd = new Card(CaseMachine.getWeaponFromString(cardValue).name(), CaseMachine.getCardTypeFromString(cardType));	
    				            		break;
    				            	}
    				            case "room":
    				            	{
    				            		cardToAdd = new Card(CaseMachine.getRoomFromString(cardValue).name(), CaseMachine.getCardTypeFromString(cardType));
    				            		break;
    				            	}
    				            case "suspect":
    				            	{
    				            		cardToAdd = new Card(CaseMachine.getCharFromString(cardValue).name(), CaseMachine.getCardTypeFromString(cardType));
    				            		break;
    				            	}
    				        }
						 }
						catch (Exception ex)
						{
							throw new Exception("Malformed card found. Check XML is valid." + '\n' +"Player " + plPlayerToAdd.getPlayerName() + ", Card: " + cardType);
						}
				       
    					
    					if (cardToAdd != null)
    					{
    						lstCards.add(cardToAdd);
    					}
	    			}
    				plPlayerToAdd.setCardsList(lstCards);
    				players.add(plPlayerToAdd);

    			}
    			
    			if (computerPlayers == nodes.getLength())
    			{
    				throw new Exception("You can not start a game of computers only");
    			}
    			
    			Board.getInstance().setNumberOfPlayersJoined(computerPlayers);
    			
    			
			}
		}
    	
		env = new Envelope();
		String strRoomName = (String) getStringFromXpath(gameInfo,
				"//murder_room/text()");
		env.setRoomCard(new Card(CaseMachine.getRoomFromString(
				strRoomName).name(), CardType.ROOM));

		String strWeaponName = (String) getStringFromXpath(gameInfo,
				"//murder_weapon/text()");
		env.setWeaponCard(new Card(CaseMachine
				.getWeaponFromString(strWeaponName).name(),CardType.WEAPON));

		String strCharName = (String) getStringFromXpath(gameInfo,
				"//murderer/text()");
		Character chr = CaseMachine.getCharFromString(strCharName);
		env.setCharacterCard(new Card(chr.name(),CardType.CHARACTER));

		if (bdRetVal == null && players != null && env != null) {
			bdRetVal = new BoardData();
			bdRetVal.setPlayers(players);
			bdRetVal.setEnvelope(env);
		}
    	
    	
		return bdRetVal;
    }
    
    /**
     * Take a file and convert it to Document object.
     * @param xmlFile
     * @return a document
     * @throws ParserConfigurationException 
     * @throws IOException 
     * @throws SAXException 
     */
    private static Document parseXMLFromFile(FileInputStream xmlFile) throws ParserConfigurationException, SAXException, IOException
    {
    	Document doc = null;
    		 
    	DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
    	docBuilderFactory.setNamespaceAware(true);
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        doc = docBuilder.parse(xmlFile);

		return doc;
    }
    
    /**
     * Get a node set from the entire doc.
     * @param doc
     * @param xPath
     * @return
     */
    private static Object getNodesetFromDocument(Document doc, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(doc, XPathConstants.NODESET);
		}
		catch (XPathExpressionException ex) 
		{
		}
		
		return result;
    }

    /**
     * Get a node set from a given node.
     * @param node - a father node to seach for son nodes in.
     * @param xPath - the path to the son node. 
     * @return - a nodeset that is a son of @node
     */
    private static Object getNodesetFromDocument(Node node, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(node, XPathConstants.NODESET);
		}
		catch (XPathExpressionException ex)
		{
		}
		return result;
    }
    
    /**
     * Get a string value from an XPATH in the xml.
     * @param doc
     * @param xPath
     * @return
     */
    private static Object getStringFromXpath(Document doc, String xPath)
    {
    	Object result = null;
    	XPathFactory factory = XPathFactory.newInstance();
    	XPath xpath = factory.newXPath();
    	XPathExpression expr;
		try 
		{
			expr = xpath.compile(xPath);
			result = expr.evaluate(doc, XPathConstants.STRING);
		}
		catch (XPathExpressionException ex)
		{
		}
		return result;
    }
}
